Covariância e Contravariância em C#, Parte Seis:Variância de Interface

05/14/2008 16:12:00 By Felipe Pessoto

Nos posts passados nós discutimos como seria possível tratar um delegate como contravariante nos seus argumentos e covariante no seu tipo de retorno. Um delegate é basicamente um objeto que representa uma chamada de função. Nós podemos fazer estes mesmos tipos de coisas para outros que representam chamadas de funções. Interfaces, por exemplo, são contratos que especificam qual o conjunto de chamadas de função que estão disponíveis em um objeto em particular.

Isto quer dizer que nós devemos extender a noção de variância para definições de interface também, usando as mesmas regras que nós temos para delegates. Por exemplo, considere:

public interface IEnumerator<T> : IDisposable, IEnumerator
{
    new T Current { get; }
}

Aqui nós temos uma interface genérica onde o único uso de parâmetro está em uma posição de saída. Poderíamos, assim, considerar o parâmetro covariante. Isso significaria que seria válido atribuir um objeto que implementa IEnumerator<Girafa> para uma variável do tipo IEnumerator<Animal>. Desde que o usuário daquela variável irá sempre esperar um Animal, e a nossa implementação irá sempre produzir uma Girafa, fica tudo ok.

Então temos IEnumerator<+T>, então nós podemos perceber que IEnumerable<T> é definido como:

public interface IEnumerable<T> : IEnumerable
{
    new IEnumerator<T> GetEnumerator();
}

Novamente, o parâmetro aparece somente em uma posição de saída, então nós poderiamos ter IEnumerable<+T> covariante também.

Isto então abre uma janela de assassinatos de bons cenários. Hoje, este código deve falhar na compilação:

void AlimentarAnimais(IEnumerable<Animal> animals)
{
    foreach (Animal animal in animals)
        if (animal.Faminto)
            Alimentar(animal);
}
...
IEnumerable<Girafa> GirafasAdultas = from g in girafas where g.Age > 5 select g;
AlimentarAnimais(GirafasAdultas);

Porque GirafasAdultas implementa IEnumerable<Girafa>, não IEnumerable<Animal>. No C# 3.0 você tem que fazer um idiota e dispendiosa operação de conversão para fazer isto compilar, algo como:

AlimentarAnimais(GirafasAdultas.Cast<Animal>());

ou

AlimentarAnimais(from g in GirafasAdultas select (Animal)g);

Ou seja o que for. Esta declaração explícita não deveria ser necessária. Diferente de arrays (que são leitura-escrita) é perfeitamente seguro tratar uma lista de Girafas read-only como uma lista de Animais.

Do mesmo modo, poderíamos tornar:

public interface IComparer<-T>
{
    int Compare(T x, T y);
}

uma interface contravariante, desde que o tipo fosse usado somente em posições de entrada. Você poderia, então, implementar um objeto que compara dois Animais e usá-lo em um contexto onde você precisa de um objeto que compare duas Girafas sem se preocupar com problemas no sistema de tipos.

Covariância e Contravariância em C#, Parte Cinco: Contravariância Dupla

04/25/2008 08:14:00 By Felipe Pessoto

Na parte quatro da série, falamos sobre como poderiamos ter um tipo de delegate que aceitaria ter valores covariantes no seu tipo de retorno e contravariantes no tipo recebido por argumento. Por exemplo, nós podemos ter um delegate action contravariante:

delegate void Action< -A > (A a);

e então temos

Action<Animal> action1 = (Animal a)=>{ Console.WriteLine(a.NomeLatin); };
Action<Girafa> action2 = action1;

Porque o invocador do action2 irá sempre passar algo que o action1 possa manipular.

Baseado no que vimos até agora no que diz respeito à variância concluimos que "o que está entrando deve ser contravariante, o que sai deve ser variante". Embora pareça que este seria um uso comum de variância que deveria ser implementada numa futura versão do C#, a realidade é um pouco mais complicada. Há uma situação onde é válido usar um argumento covariante como parâmetro de um delegate.

Suponha que você queira criar uma programação funcional de "ordem maior". Por exemplo, talvez você queira definir uma meta-ação – um delegate que recebe actions e faz alguma coisa com eles:

delegate void Meta<A>(Action<A> action);

Por exemplo:

Meta<Mamifero> meta1 = (Action<Mamifero> action)=>{action(new Girafa());};

// A próxima linha é válida porque Action<Animal> é menor que Action<Mamifero>;
// lembrando que Action é contravariante
meta1(action1); 

Então este Meta recebe um Action de Mamiferos, ou o action1 acima, o qual exibe o nome em Latin de qualquer Animal, o que quer dizer que pode ser usado para os Mamifero – e então invoca aquela action usando o new Girafa.

É evidente que o parâmetro de tipo A é usado em uma posição de entrada na definição de Meta<A>, então nós devemos ser capazes de usar contravariancia, certo? Suponha que sim. Isso significa que essa atribuição seria válida:

Meta<Tigre> meta2 = meta1; // deve ser válida se Meta é contraviante no parâmetro

Mas isso significa que isso seria válido:

Action<Tigre> action3 = Tigre=>{ Tigre.Rosnar(); };
meta2(action3);

Seguindo a lógica você verá que no final acabamos chamando (new Girafa()).Rosnar(), o qual claramente viola ambas as regras: do sistema e da natureza. Pode ser um pouco complicado até entender toda a lógica, mas escrevendo o código pode ajudar:

static void Main(string[] args)
        {
            Action<int> a;
 
            Action<Animal> action1 = metodo;
            Action<Girafa> action2 = action1;
 
            Meta<Mamifero> meta1 = metodo2;
 
            meta1(action1);
 
 
            Meta<Tigre> meta2 = meta1;
            Action<Tigre> action3 = metodo3;
            meta2(action3);
 
            Meta<Animal> meta3 = meta1;
 
 
        }
 
        static void metodo3(Tigre Tigre)
        {
            Tigre.Rosnar();
        }
 
        static void metodo2(Action<Mamifero> action)
        {
            action(new Girafa());
        }
 
        static void metodo(Animal a)
        {
            Console.WriteLine(a.NomeLatin);
        }
 
        public delegate void Meta<A>(Action<A> action);

No final do Main, você pode ver que o meta2 é igual ao meta1. O meta1 chama o método 2. O método 2 vai chamar o action passado(no caso o action3 que chama o metodo3) passando um new Girafa() como parâmetro. Neste momento que acontece a inconsistência. Pois estamos chamando o método3 passando uma Girafa, e o método3 iria chamar o método new Girafa()).Rosnar().

Então Meta<A> não pode ser contravariante em A. No entanto ele pode ser covariante:

Meta<Animal> meta3 = meta1; // válido se Meta for covariante

Agora tudo funciona. meta3 recebe um Action sobre Animals e então passa um Girafa para a Action.

Contravariância é complicado. O fato de se inverter o maior/menor relacionamento entre tipos diz que um tipo de parâmetro usado em uma posição de "contravariância dupla" (sendo uma entrada de Action, que é em si uma entrada de Meta) se torna covariante. O segundo desfaz a primeira inversão.

No próximo artigo deixaremos os delegates pra trás e falaremos sobre variância nas interfaces.

Covariância e Contravariância em C#, Parte Quatro: Variância de Delegate Real

04/01/2008 10:49:00 By Felipe Pessoto

Nos dois últimos artigos da série falei sobre dois tipos de variância que o C# suporta - covariância de array e covariância (nos tipos de retorno) e contravariância (nos tipos dos argumentos) na conversão de grupo de membros para delegate.

Hoje vamos generalizar este último tipo de variância.

Hoje no C# 3.0, embora seja válido atribuir um grupo de membros sem tipo para uma função que retorne uma Girafa para uma variável do tipo Func<Animal>, não é válido atribuir uma expressão tipada do tipo Func<Giraffe> para uma Func<Animal>. Tipos Generic do delegate são são sempre invariantes no C# 3.0. Isso parece fraco.

Suponha que nós temos a possibilidade de declarar os tipos dos parâmetros dos tipos generic do delegate como sendo covariante ou contravariante. Para simplificar (e manter a consistência com a notação existente nas especificações da CLR) iremos escrever os parâmetros de tipo covariante com um + e os parâmetros de tipo contravariantes com um -.

Esta não é uma notação muito atraente. Mas por enquanto vamos usá-la. A forma de se lembrar o que o + significa é "este tipo aceita tipos maiores na atribuição", e menores para o -.

Considere, por exemplo a nossa função padrão:

delegate R Func<A, R>(A a);

Desde que R apareça somente no retorno e A apareça somente na lista de parâmetros, podemos fazer do R covariante e o A contravariante:

delegate R Func< -A, +R >(A a);

Então novamente, você pode pensar nisto como "você pode fazer o A menor ou o R maior" (ou, é claro, ambos). Por exemplo:

Func<Animal, Giraffe> f1 = qualquer;

Func<Mammal, Mammal> f2 = f1;

Normalmente no C# esta atribuição será inválida porque os delegates são parametrizados por tipos diferentes. Mas desde que nós temos Func variante em ambos tipos de parâmetros, esta atribuição deveria se tornar válida para adicionar este tipo de variância à uma futura versão do C#.


Será que isto faz sentido até agora?

Esta regra nem sempre é correta! Algumas vezes o parâmetros de entrada precisa ser de um tipo de parâmetro covariante (no nosso exemplo A é contravariante). Iremos discutir isto somente no próximo artigo.

Covariância e Contravariância em C#, Parte Três: Variância em Grupo de Métodos

02/13/2008 08:45:00 By Felipe Pessoto

Anteriormente discutimos como a covariância numa array não funciona corretamente no C# (e Java, assim como uma série de outras linguagens). Agora, um tipo de variância válida suportada no C# 2.0: conversões de grupo de métodos em delegates. Este é um tipo mais complicado de variância, por isso vou explicar com mais detalhes.

Suponha que você tem um método que retorna um objeto Girafa.

static Girafa CriarGirafa() { }

E que você tem um delegate representando uma função que não recebe argumentos e retorna um Animal. Isto é, Func<animal>. Deveria esta conversão ser válida?

Func<animal> func = CriarGirafa;

Ao invocar Func é esperado que um Animal seja retornado. A função atual chamada pelo delegate sempre retorna uma Girafa, a qual é um Animal, então o invocador da função nunca recebe nada que ele não seja capaz de lidar. Não há problemas no sistema de tipo aqui. Portanto nós podemos criar métodos para delegate usando conversões covariantes nos seus tipos de retorno.


Agora vamos supor que você tem dois métodos, um que recebe Girafa e um que recebe um Animal:

void Foo(Girafa g) { }
void Bar(Animal a) { }

e um delegate que retorna vazio e recebe um argumento do tipo Mamifero:

Action<Mamifero> action1 = Foo; // inválido
Action<Mamifero> action2 = Bar; // válido

Por quê a primeira atribuição é inválida? Porque quem invocar o action1 pode passar por exemplo um tipo Tigre(já que seria derivado de Mamifero), mas Foo não pode receber um Tigre, somente uma Girafa(e seus derivados)! A segunda atribuição é válida porque Bar pode receber qualquer Animal.

No exemplo anterior preservamos a direção de atribuição: Girafa é menor que Animal, então o método que retorna Girafa é menor que o delegate que retorna um Animal. Neste exemplo, nós revertemos a direção de atribuição: Mamifero é menor que Animal, então o método que recebe Animal é menor que o delegate que recebe um Mamifero. Porque a direção está invertida, conversões de grupo de métodos para delegate são contravariantes nos tipos de seus argumentos.

Percebe que tudo falado acima aplica-se apenas em tipos por referência. Nunca diga algo como "Bem, todo int cabe em um long, então um método que retorna um int pode ser atribuído para uma variável do tipo Func<long>".

Covariância e Contravariância em C#, Parte Dois: Covariância de Array

01/03/2008 09:52:00 By Felipe Pessoto

C# implementa variância de duas maneiras. Hoje apresentarei a maneira incorreta.

Desde o C# 1.0, arrays onde o tipo do elemento é um tipo por referência são covariantes. Isto é perfeitamente correto:

Animal[] animais = new Girafa[10];

Desde que Girafa seja menor que Animal(isto é, Girafa herda de Animal), fazer um array dela é uma operação sobre tipos covariantes. Girafa[] é menor que Animal[], então sua instância se enquadra na variável.

Infelizmente, este tipo particular de covariância não é totalmente correto. Foi acrescentada à CLR porque o Java a implementa e os designers da CLR queriam uma linguagem parecida com o Java. Então foi adicionado ao C#, porque o recurso estava disponível na CLR. Esta decisão foi muito controversa no desenvolvimento, mas não há nada que possamos fazer em relação a isso agora.

Porque está incorreto? Porque deve ser sempre permitido colocar Tartaruga em um array de Animal. Com a covariância da array na linguagem e na runtime você não pode garantir que uma array de Animal pode aceitar uma Tartaruga porque por trás dela pode ser um array de Girafa.

Isto significa que tornamos um erro que podia ser pego pelo compilador em um que só pode ser pego no momento da execução. Isso também significa que toda vez que você colocar um objeto em um array, temos de fazer uma verificação em tempo de execução para garantir que o tipo funciona ou se gera uma exceção. Isso é potencialmente caro se você está colocando muitas coisas no array.

Um exemplo que você pode testar e verificar que realmente passa pelo compilador, mas gera uma exceção em tempo de execução:

class Animal { }
class Girafa : Animal { }
class Tartaruga : Animal { }

Animal[] animais = new Girafa[10];
animais[0] = new Tartaruga();

Na próxima parte vamos discutir uma espécie de variância que foi adicionado ao C# 2.0, que não tem problemas como esse.